﻿using System.Collections.Generic;
using System.Linq;
using System.Web.Configuration;
using DotNetNuke.Entities.Users;
using DotNetNuke.Framework.Providers;
using DotNetNuke.Security.Membership;
using System;
using System.Xml.Linq;
using BrandonHaynes.Membership.Factors;

namespace BrandonHaynes.Membership
	{
	public class MultifactorMembershipProvider : AspNetMembershipProvider
		{
		private const string defaultAuthenticationType = "BrandonHaynes.Membership.MultifactorMembershipProvider";

		/// <summary>
		/// The type of authentication intercepted by this provider; other authentication types are passed through
		/// to the underlying provider for default processing
		/// </summary>
		protected string AuthenticationType { get; set; }

		public MultifactorMembershipProvider()
			{
			// Allow the provider to override authentication type, though this would be of marginal utility
			AuthenticationType = ProviderConfiguration.GetProviderConfiguration("members")
				.GetByType(this.GetType())
				.Attributes["authenticationType"] ?? defaultAuthenticationType;

			// We don't fool around with authentication that doesn't include the ASP.NET membership provider.
			if(Factors.Values.OfType<MembershipFactor>().IsEmpty())
				throw new ArgumentException("A BrandonHaynes.Membership.MembershipFactor provider is required.");
			}

		public override UserInfo UserLogin(int portalId, string username, string password, string authType, string verificationCode, ref UserLoginStatus loginStatus)
			{
			// We only deal with our targeted authentication method; fall back to default authentication otherwise
			if (authType == this.AuthenticationType)
				{
				var user = new MultifactorUser(GetUserByUserName(portalId, username, false));
				var credential = new Credential(user.PortalID, password, verificationCode);
				return MultifactorLogin(user, credential, ref loginStatus);
				}
			else
				return base.UserLogin(portalId, username, password, authType, verificationCode, ref loginStatus);
			}

		private static UserInfo MultifactorLogin(MultifactorUser user, Credential credential, ref UserLoginStatus loginStatus)
			{
			// Authenticate against all factors; stopping when any first failure occurs
			GetFactorsByUser(user).Any(factor => !AuthenticateFactor(factor, user, credential));

			// If any factor indicates that it is incomplete
			if (!credential.IsComplete)
				{
				user.MissingCredentials = credential.IncompleteFactors.ToList();
				credential.Status = UserLoginStatus.LOGIN_USERNOTAPPROVED;
				}

			// Set our ref parameter and return the user
			loginStatus = credential.Status;
			return user;
			}

		/// <summary>
		/// Authenticates a user against a given factor
		/// </summary>
		/// <param name="factor">The factor to authenticate</param>
		/// <param name="user">The principal to authenticate</param>
		/// <param name="credential">A credential to use for authentication</param>
		/// <returns>The credential's validity</returns>
		protected static bool AuthenticateFactor(IAuthenticationFactor factor, UserInfo user, Credential credential)
			{
			// Only attempt authentication against a credential that hasn't been marked invalid
			//		Does this work correctly when authenticating against multiple factors?
			if(credential.IsValid)
				factor.Authenticate(user, credential);
	
			return credential.IsValid;
			}

		/// <summary>
		/// Given a user, returns a set of distinct IAuthenticationFactors that apply to that user
		/// </summary>
		/// <returns></returns>
		protected static IEnumerable<IAuthenticationFactor> GetFactorsByUser(UserInfo user)
			{
			// Select the union of the factors required by each role 
			// Add host factors, if applicable
			// Append the "Membership" factor, which is always required
			return user.Roles
				.SelectMany(role => RoleMap[role])
				.Union(user.IsSuperUser ? RoleMap["host"] : Enumerable.Empty<string>())
				.Union("Membership".ToEnumerable())
				.Distinct()
				.Select(name => Factors[name]);
			}

		/// <summary>
		/// Returns a dictionary mapping factor names to IAuthentiationFactor instances
		/// </summary>
		protected static IDictionary<string, IAuthenticationFactor> Factors
			{ get { return (IDictionary<string, IAuthenticationFactor>)WebConfigurationManager.GetSection("brandonHaynes/authenticationFactors"); } }

		/// <summary>
		/// Returns an index mapping role names to an enumeration of factor names
		/// </summary>
		protected static ILookup<string, string> RoleMap
			{ get { return (ILookup<string, string>)WebConfigurationManager.GetSection("brandonHaynes/authenticationMap"); } }
		}
	}
